using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using Needle.Core.Injection;
using Needle.Utils;

namespace Needle.Core.Activation
{

    /// <summary>
    /// Activation info obtained through reflection.
    /// </summary>
    [DebuggerDisplay("ReflectionActivationInfo {Id}, Type: {Type}")]
    public class ReflectionActivationInfo : IActivationInfo
    {

        /// <summary>
        /// initializes a new ReflectionActivationInfo instance for the type  given.
        /// </summary>
        public ReflectionActivationInfo(Type toActivate)
            : this(new InstanceId(toActivate), toActivate)
        {
        }

        /// <summary>
        /// initializes a new ReflectionActivationInfo instance for the id and type  given.
        /// </summary>
        public ReflectionActivationInfo(InstanceId id, Type toActivate)
        {
            Ensure.NotNull(id, "id");
            Ensure.NotNull(toActivate, "toActivate");
            m_Id = id;
            m_Type = toActivate;
            Initialize();
        }

        #region Properties

        private InstanceId m_Id;
        /// <summary>
        /// The dependency  this activation info is for.
        /// </summary>
        public InstanceId Id
        {
            get { return m_Id; }
        }

        private Type m_Type;
        /// <summary>
        /// The concrete type to activate.
        /// </summary>
        public Type Type
        {
            get { return m_Type; }
        }

        private IInjectionSite m_Constructor;
        /// <summary>
        /// The construction injection site.
        /// </summary>
        public IInjectionSite Constructor
        {
            get { return m_Constructor; }
        }

        private IList<IInjectionSite> m_Methods;
        /// <summary>
        /// the injectable methods.
        /// </summary>
        public IList<IInjectionSite> Methods
        {
            get { return m_Methods; }
        }

        #endregion

        #region Initialize

        private void Initialize()
        {

            CheckTypeIsConstructable(this.Type);
            m_Constructor = GetConstructorSite();
            List<IInjectionSite> sites = new List<IInjectionSite>();
            sites.AddRange(GetPropertySites());
            sites.AddRange(GetMethodSites());
            sites.Sort();
            m_Methods = new ReadOnlyCollection<IInjectionSite>(sites);
        }

        private void CheckTypeIsConstructable(Type t)
        {
            bool isConcrete = t.IsClass && !t.ContainsGenericParameters && !t.IsAbstract;
            bool hasConstructors = t.GetConstructors().Length > 0;
            if (isConcrete && hasConstructors)
            {
                return; // ok;
            }

            throw new ActivationException(
                String.Format("Type {0} is not concrete or has no suitable constructors", t.FullName));
        }

        #endregion

        #region ConstructorInjection

        private IInjectionSite GetConstructorSite()
        {
            ConstructorInfo ci = GetInjectedConstructor();
            InjectionSite site = new InjectionSite(ci, -1);
            return site;
        }

        /// <summary>
        /// Finds the Inject tagged constructor or the default constructor.
        /// </summary>
        /// <returns></returns>
        private ConstructorInfo GetInjectedConstructor()
        {
            ConstructorInfo injected = null;
            ConstructorInfo[] cis = Type.GetConstructors();

            foreach (ConstructorInfo ci in cis)
            {
                InjectAttribute inject = InjectAttribute.GetFrom(ci);
                if (inject != null)
                {
                    if (injected == null)
                    {
                        injected = ci;
                    }
                    else
                    {
                        throw new ActivationException("Multiple injectable constructors found.");
                    }
                }
            }

            if (injected == null)
            { // try to find the default constructor
                injected = Type.GetConstructor(Type.EmptyTypes);
            }
            if (injected == null)
            {
                throw new ActivationException("No injectable constructor found");
            }
            return injected;
        }

        #endregion

        #region PropertyInjection

        private IList<IInjectionSite> GetPropertySites()
        {
            List<IInjectionSite> sites = new List<IInjectionSite>();
            PropertyInfo[] props = Type.GetProperties(BindingFlags.Instance | BindingFlags.Public);

            IInjectionSite site = null;
            foreach (PropertyInfo prop in props)
            {
                if (TryGetPropertyInjectionSite(prop, ref site))
                {
                    sites.Add(site);
                }
            }

            return sites.ToArray();
        }

        private bool TryGetPropertyInjectionSite(PropertyInfo prop, ref IInjectionSite site)
        {
            InjectAttribute attr = InjectAttribute.GetFrom(prop);

            MethodBase setter = null;
            if (prop.CanWrite && prop.GetIndexParameters().Length == 0)
            {
                setter = prop.GetSetMethod();
                if (attr == null)
                {
                    attr = InjectAttribute.GetFrom(setter);
                }
            }
            site = null;
            if (attr != null && setter != null)
            {
                List<IParameter> parameters = new List<IParameter>();
                ParameterInfo pi = setter.GetParameters()[0];
                InstanceId id = new InstanceId(
                        attr.Type == null ? pi.ParameterType : attr.Type,
                        attr.Name
                    );
                parameters.Add(new Parameter(pi, id));
                site = new InjectionSite(setter, attr.Order, parameters);
            }
            return site != null;
        }

        #endregion

        #region MethodInjection

        private IList<IInjectionSite> GetMethodSites()
        {
            List<IInjectionSite> sites = new List<IInjectionSite>();
            System.Reflection.MethodInfo[] methods = Type.GetMethods(BindingFlags.Instance | BindingFlags.Public);

            IInjectionSite site = null;
            foreach (System.Reflection.MethodInfo method in methods)
            {

                if (!method.IsSpecialName && TryGetMethodInjectionSite(method, ref site))
                {
                    sites.Add(site);
                }
            }

            // sortby injection order.
            sites.Sort(delegate(IInjectionSite a, IInjectionSite b)
            {
                return a.Order - b.Order;
            });

            return sites.ToArray();
        }

        private bool TryGetMethodInjectionSite(System.Reflection.MethodInfo method, ref IInjectionSite site)
        {
            InjectAttribute attr = InjectAttribute.GetFrom(method);

            site = null;
            if (attr != null)
            {
                site = new InjectionSite(method, attr.Order);
            }
            return site != null;
        }

        #endregion

    }

}
